package com.advert.service.security;

import com.advert.dto.mail.Mail;
import com.advert.dto.mail.PasswordRecoveryMail;
import com.advert.dto.mail.UserActivationMail;
import com.advert.exception.ApplicationRuntimeException;
import com.advert.exception.AuthenticationException;
import com.advert.model.security.*;
import com.advert.model.security.Role;
import com.advert.provider.security.SecurityProvider;
import com.advert.security.AppSecurityManager;
import com.advert.security.Credentials;
import com.advert.service.mail.MailService;
import com.advert.util.Event;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.*;
import org.jboss.seam.core.Events;
import org.jboss.seam.log.Log;
import sun.misc.BASE64Encoder;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

@Name("SecurityService")
@Scope(ScopeType.EVENT)
@AutoCreate
public class SecurityService implements Serializable {
    @Logger
    private Log logger;

    @In("SecurityProvider")
    private SecurityProvider securityProvider;
    @In("SecurityManager")
    private AppSecurityManager securityManager;
    @In("MailService")
    private MailService mailService;


    @Transactional
    public UserEntity login(Credentials credentials) throws AuthenticationException {
        UserEntity user = securityProvider.getUserByLogin(credentials.getLogin());
        if (user != null) {
            String passHash = getHash(saltPassword(credentials.getLogin(), credentials.getPassword()));
            if (passHash.equals(user.getPassword())) {
                if (user.getStatus() == Status.ACTIVE) {
                    user.setLoginAttempts(null);
                    user.setLastLoginDate(new Date());
                    user = securityProvider.updateUser(user);
                } else {
                    throw new AuthenticationException(AuthenticationException.Reason.USER_NOT_ACTIVE);    
                }
            } else {
                Integer attempts = loginAttempts(user);
                user.setLoginAttempts(attempts);
                securityProvider.updateUser(user);
                throw new AuthenticationException(AuthenticationException.Reason.PASSWORD_INCORRECT, attempts);
            }
        } else {
            throw new AuthenticationException(AuthenticationException.Reason.NO_SUCH_LOGIN);    
        }
        return user;
    }

    @Transactional
    public UserEntity registerUser(Credentials credentials) throws AuthenticationException {
        UserEntity user = securityProvider.getUserByLogin(credentials.getLogin());
        if (user != null) {
            throw new AuthenticationException(AuthenticationException.Reason.LOGIN_ALREADY_EXISTS);
        } else {
            Date today = new Date();
            String passHash = getHash(saltPassword(credentials.getLogin(), credentials.getPassword()));

            UserInfoEntity userInfo = new UserInfoEntity(credentials.getEmail());
            userInfo.setCreateDate(today);
            userInfo.setUpdateDate(today);
            user = new UserEntity(credentials.getLogin(), passHash, Role.CLIENT, Status.PENDING);
            user.setUserInfo(userInfo);
            user.setCreateDate(today);
            user.setUpdateDate(today);
            String uuid = UUID.randomUUID().toString();
            user.setActivationUid(uuid);

            user = securityProvider.createUser(user);
            user.getUserInfo().setOwnerId(securityManager.userId());
            
            Mail activationMail = new UserActivationMail(credentials.getEmail(), null, null, String.valueOf(user.getId()), uuid);
            Events.instance().raiseEvent(Event.MAIL_BUILD_REQUIRED, activationMail);
            mailService.sendMail(activationMail);
        }
        return user;
    }

    @Transactional
    public void recoverPassword(Credentials credentials) throws AuthenticationException {
        UserEntity user = securityProvider.getUserByLoginAndMail(credentials.getLogin(), credentials.getEmail());
        if (user != null) {
            String uuid = UUID.randomUUID().toString();
            String newPassword = uuid.substring(0, 7);
            
            user.setPassword(getHash(saltPassword(credentials.getLogin(), newPassword)));
            user.setLoginAttempts(null);
            user.setUpdateDate(new Date());
            securityProvider.updateUser(user);

            Mail activationMail = new PasswordRecoveryMail(credentials.getEmail(), null, null, newPassword);
            Events.instance().raiseEvent(Event.MAIL_BUILD_REQUIRED, activationMail);
            mailService.sendMail(activationMail);
        } else {
            throw new AuthenticationException(AuthenticationException.Reason.NO_SUCH_LOGIN_OR_MAIL);
        }
    }

    @Transactional
    public UserEntity activateUser(Integer id, String activationCode) throws AuthenticationException {
        UserEntity user = securityProvider.getUserById(id);
        if (user != null && user.getActivationUid() != null && user.getStatus() != Status.ACTIVE) {
            UUID storedActivationCode = UUID.fromString(user.getActivationUid());
            UUID receivedActivationCode = UUID.fromString(activationCode);
            if (storedActivationCode.equals(receivedActivationCode)) {
                user.setActivationUid(null);
                user.setStatus(Status.ACTIVE);
                securityProvider.updateUser(user);
            } else {
                throw new AuthenticationException(AuthenticationException.Reason.ACTIVATION_INVALID);    
            }
        } else {
            throw new AuthenticationException(AuthenticationException.Reason.ACTIVATION_INVALID);
        }
        return user;
    }

    @Transactional
    public void changePassword(Integer userId, Credentials credentials) throws AuthenticationException {
        UserEntity user = securityProvider.getUserById(userId);
        String passHash = getHash(saltPassword(credentials.getLogin(), credentials.getPassword()));
        if (!user.getPassword().equals(passHash)) {
            throw new AuthenticationException(AuthenticationException.Reason.PASSWORD_INCORRECT);
        }
        credentials.setPassword(credentials.getNewPassword());
        user.setPassword(getHash(saltPassword(credentials.getLogin(), credentials.getPassword())));
        user.setUpdateDate(new Date());
        securityProvider.updateUser(user);
    }

    public Map<String, Map<Role, Map<AccessType,SecurityEntity>>> getSecurityCollection() {
        Map<String, Map<Role, Map<AccessType,SecurityEntity>>> retVal = new HashMap<String, Map<Role, Map<AccessType,SecurityEntity>>>();
        List<SecurityEntity> securities = securityProvider.getAllSecurity();
        for (SecurityEntity security : securities) {
            Map<Role, Map<AccessType,SecurityEntity>> roleMap = retVal.get(security.getEntity());
            if (roleMap == null) {
                roleMap = new HashMap<Role, Map<AccessType,SecurityEntity>>();
                retVal.put(security.getEntity(), roleMap);
            }
            Map<AccessType,SecurityEntity> accessTypeMap = roleMap.get(security.getRole());
            if (accessTypeMap == null) {
                accessTypeMap = new HashMap<AccessType,SecurityEntity>();
                roleMap.put(security.getRole(), accessTypeMap);
            }
            accessTypeMap.put(security.getAccessType(), security);
        }
        return retVal;
    }

    public UserInfoEntity getUserInfo(Integer userId) {
        return securityProvider.getUserInfo(userId);   
    }

    @Transactional
    public UserInfoEntity updateUserInfo(UserInfoEntity info) {
        return securityProvider.updateUserInfo(info);
    }


    /**
     *  Current realization uses SHA algorithm.
     */
    private String getHash(String value) {
        String retVal = null;

        if (value != null) {
            MessageDigest md = null;
            try {
                md = MessageDigest.getInstance("SHA");
            } catch(NoSuchAlgorithmException e) {
                logger.fatal("Error creating password hash.", e.getCause());
                throw new ApplicationRuntimeException("Error creating password hash.", e.getCause());
            }
            try {
                md.update(value.getBytes("UTF-8"));
            } catch(UnsupportedEncodingException e) {
                logger.fatal("Error creating password hash.", e.getCause());
                throw new ApplicationRuntimeException("Error creating password hash.", e.getCause());
            }

            byte raw[] = md.digest();
            retVal = (new BASE64Encoder()).encode(raw);
        }

        return retVal;
    }

    private String saltPassword(String login, String password) {
        StringBuffer retVal = new StringBuffer();

        char[] salt = {'!', '@', '#', '$', '%', '^', '&', '*', '(', ')'};
        char[] loginChars = login.toCharArray();
        char[] passwordChars = password.toCharArray();
        
        for (int i = 0; i < passwordChars.length; i++) {
            retVal.append(passwordChars[i]);
            if (i < salt.length) {
                retVal.append(salt[i]);
            }
            if (i < loginChars.length) {
                retVal.append(loginChars[i]);
            }
        }
        return retVal.toString();
    }

    private Integer loginAttempts(UserEntity user) {
        Integer attempts = user.getLoginAttempts();
        if (attempts == null) {
            attempts = 1;
        } else {
            attempts ++;
        }
        return attempts;
    }
}
